/*
 *  Copyright 2005 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.mybatis.generator.api.dom.java;

import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;
import static org.mybatis.generator.internal.util.messages.Messages.getString;

import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

/**
 * @author Jeff Butler
 */
public class FullyQualifiedJavaType implements Comparable<FullyQualifiedJavaType>
{
    private static final String JAVA_LANG = "java.lang"; //$NON-NLS-1$
    
    private static FullyQualifiedJavaType intInstance = null;
    
    private static FullyQualifiedJavaType stringInstance = null;
    
    private static FullyQualifiedJavaType booleanPrimitiveInstance = null;
    
    private static FullyQualifiedJavaType objectInstance = null;
    
    private static FullyQualifiedJavaType dateInstance = null;
    
    private static FullyQualifiedJavaType criteriaInstance = null;
    
    private static FullyQualifiedJavaType generatedCriteriaInstance = null;
    
    /**
     * The short name without any generic arguments
     */
    private String baseShortName;
    
    /**
     * The fully qualified name without any generic arguments
     */
    private String baseQualifiedName;
    
    private boolean explicitlyImported;
    
    private String packageName;
    
    private boolean primitive;
    
    private PrimitiveTypeWrapper primitiveTypeWrapper;
    
    private List<FullyQualifiedJavaType> typeArguments;
    
    // the following three values are used for dealing with wildcard types
    private boolean wildcardType;
    
    private boolean boundedWildcard;
    
    private boolean extendsBoundedWildcard;
    
    /**
     * Use this constructor to construct a generic type with the specified type parameters
     * 
     * @param fullTypeSpecification
     */
    public FullyQualifiedJavaType(String fullTypeSpecification)
    {
        super();
        typeArguments = new ArrayList<FullyQualifiedJavaType>();
        parse(fullTypeSpecification);
    }
    
    /**
     * @return Returns the explicitlyImported.
     */
    public boolean isExplicitlyImported()
    {
        return explicitlyImported;
    }
    
    /**
     * This method returns the fully qualified name - including any generic type parameters
     * 
     * @return Returns the fullyQualifiedName.
     */
    public String getFullyQualifiedName()
    {
        StringBuilder sb = new StringBuilder();
        if (wildcardType)
        {
            sb.append('?');
            if (boundedWildcard)
            {
                if (extendsBoundedWildcard)
                {
                    sb.append(" extends "); //$NON-NLS-1$
                }
                else
                {
                    sb.append(" super "); //$NON-NLS-1$
                }
                sb.append(baseQualifiedName);
            }
        }
        else
        {
            sb.append(baseQualifiedName);
        }
        if (typeArguments.size() > 0)
        {
            boolean first = true;
            sb.append('<');
            for (FullyQualifiedJavaType fqjt : typeArguments)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    sb.append(", "); //$NON-NLS-1$
                }
                sb.append(fqjt.getFullyQualifiedName());
            }
            sb.append('>');
        }
        return sb.toString();
    }
    
    /**
     * Returns a list of Strings that are the fully qualified names of this type, and any generic type argument associated with this type.
     */
    public List<String> getImportList()
    {
        List<String> answer = new ArrayList<String>();
        if (isExplicitlyImported())
        {
            int index = baseShortName.indexOf('.');
            if (index == -1)
            {
                answer.add(baseQualifiedName);
            }
            else
            {
                // an inner class is specified, only import the top
                // level class
                StringBuilder sb = new StringBuilder();
                sb.append(packageName);
                sb.append('.');
                sb.append(baseShortName.substring(0, index));
                answer.add(sb.toString());
            }
        }
        for (FullyQualifiedJavaType fqjt : typeArguments)
        {
            answer.addAll(fqjt.getImportList());
        }
        return answer;
    }
    
    /**
     * @return Returns the packageName.
     */
    public String getPackageName()
    {
        return packageName;
    }
    
    /**
     * @return Returns the shortName - including any type arguments.
     */
    public String getShortName()
    {
        StringBuilder sb = new StringBuilder();
        if (wildcardType)
        {
            sb.append('?');
            if (boundedWildcard)
            {
                if (extendsBoundedWildcard)
                {
                    sb.append(" extends "); //$NON-NLS-1$
                }
                else
                {
                    sb.append(" super "); //$NON-NLS-1$
                }
                sb.append(baseShortName);
            }
        }
        else
        {
            sb.append(baseShortName);
        }
        if (typeArguments.size() > 0)
        {
            boolean first = true;
            sb.append('<');
            for (FullyQualifiedJavaType fqjt : typeArguments)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    sb.append(", "); //$NON-NLS-1$
                }
                sb.append(fqjt.getShortName());
            }
            sb.append('>');
        }
        return sb.toString();
    }
    
    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        if (!(obj instanceof FullyQualifiedJavaType))
        {
            return false;
        }
        FullyQualifiedJavaType other = (FullyQualifiedJavaType)obj;
        return getFullyQualifiedName().equals(other.getFullyQualifiedName());
    }
    
    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode()
    {
        return getFullyQualifiedName().hashCode();
    }
    
    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString()
    {
        return getFullyQualifiedName();
    }
    
    /**
     * @return Returns the primitive.
     */
    public boolean isPrimitive()
    {
        return primitive;
    }
    
    /**
     * @return Returns the wrapperClass.
     */
    public PrimitiveTypeWrapper getPrimitiveTypeWrapper()
    {
        return primitiveTypeWrapper;
    }
    
    public static final FullyQualifiedJavaType getIntInstance()
    {
        if (intInstance == null)
        {
            intInstance = new FullyQualifiedJavaType("int"); //$NON-NLS-1$
        }
        return intInstance;
    }
    
    public static final FullyQualifiedJavaType getNewMapInstance()
    {
        // always return a new instance because the type may be parameterized
        return new FullyQualifiedJavaType("java.util.Map"); //$NON-NLS-1$
    }
    
    public static final FullyQualifiedJavaType getNewListInstance()
    {
        // always return a new instance because the type may be parameterized
        return new FullyQualifiedJavaType("java.util.List"); //$NON-NLS-1$
    }
    
    public static final FullyQualifiedJavaType getNewHashMapInstance()
    {
        // always return a new instance because the type may be parameterized
        return new FullyQualifiedJavaType("java.util.HashMap"); //$NON-NLS-1$
    }
    
    public static final FullyQualifiedJavaType getNewArrayListInstance()
    {
        // always return a new instance because the type may be parameterized
        return new FullyQualifiedJavaType("java.util.ArrayList"); //$NON-NLS-1$
    }
    
    public static final FullyQualifiedJavaType getNewIteratorInstance()
    {
        // always return a new instance because the type may be parameterized
        return new FullyQualifiedJavaType("java.util.Iterator"); //$NON-NLS-1$
    }
    
    public static final FullyQualifiedJavaType getStringInstance()
    {
        if (stringInstance == null)
        {
            stringInstance = new FullyQualifiedJavaType("java.lang.String"); //$NON-NLS-1$
        }
        return stringInstance;
    }
    
    public static final FullyQualifiedJavaType getBooleanPrimitiveInstance()
    {
        if (booleanPrimitiveInstance == null)
        {
            booleanPrimitiveInstance = new FullyQualifiedJavaType("boolean"); //$NON-NLS-1$
        }
        return booleanPrimitiveInstance;
    }
    
    public static final FullyQualifiedJavaType getObjectInstance()
    {
        if (objectInstance == null)
        {
            objectInstance = new FullyQualifiedJavaType("java.lang.Object"); //$NON-NLS-1$
        }
        return objectInstance;
    }
    
    public static final FullyQualifiedJavaType getDateInstance()
    {
        if (dateInstance == null)
        {
            dateInstance = new FullyQualifiedJavaType("java.util.Date"); //$NON-NLS-1$
        }
        return dateInstance;
    }
    
    public static final FullyQualifiedJavaType getCriteriaInstance()
    {
        if (criteriaInstance == null)
        {
            criteriaInstance = new FullyQualifiedJavaType("Criteria"); //$NON-NLS-1$
        }
        return criteriaInstance;
    }
    
    public static final FullyQualifiedJavaType getGeneratedCriteriaInstance()
    {
        if (generatedCriteriaInstance == null)
        {
            generatedCriteriaInstance = new FullyQualifiedJavaType("GeneratedCriteria"); //$NON-NLS-1$
        }
        return generatedCriteriaInstance;
    }
    
    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo(FullyQualifiedJavaType other)
    {
        return getFullyQualifiedName().compareTo(other.getFullyQualifiedName());
    }
    
    public void addTypeArgument(FullyQualifiedJavaType type)
    {
        typeArguments.add(type);
    }
    
    private void parse(String fullTypeSpecification)
    {
        String spec = fullTypeSpecification.trim();
        if (spec.startsWith("?")) //$NON-NLS-1$
        {
            wildcardType = true;
            spec = spec.substring(1).trim();
            if (spec.startsWith("extends ")) //$NON-NLS-1$
            {
                boundedWildcard = true;
                extendsBoundedWildcard = true;
                spec = spec.substring(8);
            }
            else if (spec.startsWith("super ")) //$NON-NLS-1$
            {
                boundedWildcard = true;
                extendsBoundedWildcard = false;
                spec = spec.substring(6);
            }
            else
            {
                boundedWildcard = false;
            }
            parse(spec);
        }
        else
        {
            int index = fullTypeSpecification.indexOf('<');
            if (index == -1)
            {
                simpleParse(fullTypeSpecification);
            }
            else
            {
                simpleParse(fullTypeSpecification.substring(0, index));
                genericParse(fullTypeSpecification.substring(index));
            }
        }
    }
    
    private void simpleParse(String typeSpecification)
    {
        baseQualifiedName = typeSpecification.trim();
        if (baseQualifiedName.contains(".")) //$NON-NLS-1$
        {
            packageName = getPackage(baseQualifiedName);
            baseShortName = baseQualifiedName.substring(packageName.length() + 1);
            int index = baseShortName.lastIndexOf('.');
            if (index != -1)
            {
                baseShortName = baseShortName.substring(index + 1);
            }
            if (JAVA_LANG.equals(packageName))
            { // $NON-NLS-1$
                explicitlyImported = false;
            }
            else
            {
                explicitlyImported = true;
            }
        }
        else
        {
            baseShortName = baseQualifiedName;
            explicitlyImported = false;
            packageName = ""; //$NON-NLS-1$
            if ("byte".equals(baseQualifiedName)) //$NON-NLS-1$
            {
                primitive = true;
                primitiveTypeWrapper = PrimitiveTypeWrapper.getByteInstance();
            }
            else if ("short".equals(baseQualifiedName)) //$NON-NLS-1$
            {
                primitive = true;
                primitiveTypeWrapper = PrimitiveTypeWrapper.getShortInstance();
            }
            else if ("int".equals(baseQualifiedName)) //$NON-NLS-1$
            {
                primitive = true;
                primitiveTypeWrapper = PrimitiveTypeWrapper.getIntegerInstance();
            }
            else if ("long".equals(baseQualifiedName)) //$NON-NLS-1$
            {
                primitive = true;
                primitiveTypeWrapper = PrimitiveTypeWrapper.getLongInstance();
            }
            else if ("char".equals(baseQualifiedName)) //$NON-NLS-1$
            {
                primitive = true;
                primitiveTypeWrapper = PrimitiveTypeWrapper.getCharacterInstance();
            }
            else if ("float".equals(baseQualifiedName)) //$NON-NLS-1$
            {
                primitive = true;
                primitiveTypeWrapper = PrimitiveTypeWrapper.getFloatInstance();
            }
            else if ("double".equals(baseQualifiedName)) //$NON-NLS-1$
            {
                primitive = true;
                primitiveTypeWrapper = PrimitiveTypeWrapper.getDoubleInstance();
            }
            else if ("boolean".equals(baseQualifiedName)) //$NON-NLS-1$
            {
                primitive = true;
                primitiveTypeWrapper = PrimitiveTypeWrapper.getBooleanInstance();
            }
            else
            {
                primitive = false;
                primitiveTypeWrapper = null;
            }
        }
    }
    
    private void genericParse(String genericSpecification)
    {
        int lastIndex = genericSpecification.lastIndexOf('>');
        if (lastIndex == -1)
        {
            throw new RuntimeException(getString("RuntimeError.22", genericSpecification)); //$NON-NLS-1$
        }
        String argumentString = genericSpecification.substring(1, lastIndex);
        // need to find "," outside of a <> bounds
        StringTokenizer st = new StringTokenizer(argumentString, ",<>", true); //$NON-NLS-1$
        int openCount = 0;
        StringBuilder sb = new StringBuilder();
        while (st.hasMoreTokens())
        {
            String token = st.nextToken();
            if ("<".equals(token)) //$NON-NLS-1$
            {
                sb.append(token);
                openCount++;
            }
            else if (">".equals(token)) //$NON-NLS-1$
            {
                sb.append(token);
                openCount--;
            }
            else if (",".equals(token)) //$NON-NLS-1$
            {
                if (openCount == 0)
                {
                    typeArguments.add(new FullyQualifiedJavaType(sb.toString()));
                    sb.setLength(0);
                }
                else
                {
                    sb.append(token);
                }
            }
            else
            {
                sb.append(token);
            }
        }
        if (openCount != 0)
        {
            throw new RuntimeException(getString("RuntimeError.22", genericSpecification)); //$NON-NLS-1$
        }
        String finalType = sb.toString();
        if (stringHasValue(finalType))
        {
            typeArguments.add(new FullyQualifiedJavaType(finalType));
        }
    }
    
    /**
     * Returns the package name of a fully qualified type.
     * 
     * This method calculates the package as the part of the fully qualified name up to, but not including, the last element. Therefore, it does not support fully qualified inner classes. Not totally
     * fool proof, but correct in most instances.
     * 
     * @param baseQualifiedName
     * @return
     */
    private static String getPackage(String baseQualifiedName)
    {
        int index = baseQualifiedName.lastIndexOf('.');
        return baseQualifiedName.substring(0, index);
    }
}
